unit appTool;

{$mode Delphi}{$H+}

interface

uses
  Classes, SysUtils, laz.VirtualTrees, StrUtils, Math;

type

  TVirtualStringTree = TlazVirtualStringTree;


  {$I const.inc}

function Implode(Separator: string; a: TStrings): string;
function Explode(Separator, Text: string): TStringList;
function f_(const Pattern: string; const Args: array of const): string;
function SelectNode(VT: TVirtualStringTree; idx: int64;
  ParentNode: PVirtualNode = nil): boolean; overload;
function FindNode(VT: TVirtualStringTree; idx: int64;
  ParentNode: PVirtualNode): PVirtualNode;
function SelectNode(VT: TVirtualStringTree; Node: PVirtualNode;
  ClearSelection: boolean = True): boolean; overload;

function MakeInt(Str: string): int64;
function MakeFloat(Str: string): extended;
function CleanupNumber(Str: string): string;

function FormatNumber(str: string; Thousands: boolean = True): string; overload;
function UnformatNumber(Val: string): string;
function FormatNumber(int: int64; Thousands: boolean = True): string; overload;
function FormatNumber(flt: double; decimals: integer = 0;
  Thousands: boolean = True): string; overload;
function FormatByteNumber(Bytes: int64; Decimals: byte = 1): string; overload;
function FormatByteNumber(Bytes: string; Decimals: byte = 1): string; overload;
function FormatTimeNumber(Seconds: double; DisplaySeconds: boolean;
  MilliSecondsPrecision: integer = 1): string;
function TruncDef(X: real; Default: int64): int64;



var
  NumberChars: TSysCharSet;

implementation


uses
  umyTranslate;

{***
  Convert a TStringList to a string using a separator-string

  @todo Look at each caller to see if escaping is necessary.
  @param string Separator
  @param a TStringList Containing strings
  @return string
}
function Implode(Separator: string; a: TStrings): string;
var
  i: integer;
begin
  Result := '';
  for i := 0 to a.Count - 1 do
  begin
    Result := Result + a[i];
    if i < a.Count - 1 then
      Result := Result + Separator;
  end;
end;

function f_(const Pattern: string; const Args: array of const): string;
var
  TranslatedPattern: string;
begin
  // Helper for translation, replacement for Format(_())
  try
    TranslatedPattern := _(Pattern);
    Result := Format(TranslatedPattern, Args);
  except
    on E: Exception do
    begin
      //MainForm.LogSQL(E.ClassName+' in translation string with invalid format arguments: "'+TranslatedPattern+'"', lcError);
      Result := Format(Pattern, Args);
    end;
  end;
end;


function Explode(Separator, Text: string): TStringList;
var
  i: integer;
  Item: string;
begin
  // Explode a string by separator into a TStringList
  Result := TStringList.Create;
  while True do
  begin
    i := Pos(Separator, Text);
    if i = 0 then
    begin
      // Last or only segment: Add to list if it's the last. Add also if it's not empty and list is empty.
      // Do not add if list is empty and text is also empty.
      if (Result.Count > 0) or (Text <> '') then
        Result.Add(Text);
      break;
    end;
    Item := Copy(Text, 1, i - 1);
    Result.Add(Item);
    Delete(Text, 1, i - 1 + Length(Separator));
  end;
end;


function FindNode(VT: TVirtualStringTree; idx: int64;
  ParentNode: PVirtualNode): PVirtualNode;
var
  Node: PVirtualNode;
begin
  // Helper to find a node by its index
  Result := nil;
  Node := nil;
  try
    if Assigned(ParentNode) then
      Node := VT.GetFirstChild(ParentNode)
    else
      Node := VT.GetFirst;
  except
    // Sporadically, TBaseVirtualTree.GetFirst throws an exception when reading FRoot.FirstChild
    // Tab restoring is sometimes crashing for that reason.
  end;
  while Assigned(Node) do
  begin
    // Note: Grid.RootNodeCount is unfortunately Cardinal, not UInt64.
    if Node.Index = idx then
    begin
      Result := Node;
      break;
    end;
    Node := VT.GetNextSibling(Node);
  end;
end;

function SelectNode(VT: TVirtualStringTree; idx: int64;
  ParentNode: PVirtualNode = nil): boolean; overload;
var
  Node: PVirtualNode;
begin
  // Helper to focus and highlight a node by its index
  Node := FindNode(VT, idx, ParentNode);
  if Assigned(Node) then
    Result := SelectNode(VT, Node)
  else
    Result := False;
end;


function SelectNode(VT: TVirtualStringTree; Node: PVirtualNode;
  ClearSelection: boolean = True): boolean; overload;
var
  OldFocus: PVirtualNode;
  MinimumColumnIndex: TColumnIndex;
begin
  if Node = VT.RootNode then
    Node := nil;
  OldFocus := VT.FocusedNode;
  Result := True;
  if (Node <> OldFocus) and Assigned(VT.OnFocusChanging) then
  begin
    VT.OnFocusChanging(VT, OldFocus, Node, VT.FocusedColumn, VT.FocusedColumn, Result);
  end;
  if Result then
  begin
    if ClearSelection then
      VT.ClearSelection;
    VT.FocusedNode := Node;
    MinimumColumnIndex := VT.Header.Columns.GetFirstVisibleColumn(True);
    if VT.FocusedColumn < MinimumColumnIndex then
      VT.FocusedColumn := MinimumColumnIndex;
    VT.Selected[Node] := True;
    VT.ScrollIntoView(Node, False);
    if (OldFocus = Node) and Assigned(VT.OnFocusChanged) then
      VT.OnFocusChanged(VT, Node, VT.FocusedColumn);
  end;
end;

function CleanupNumber(Str: string): string;
var
  i: integer;
  HasDecimalSep: boolean;
begin
  // Ensure the passed string contains a valid number, which is convertable by StrToFloat afterwards
  // Return it as string again, as there are callers which need to handle unsigned bigint's somehow -
  // there is no unsigned 64 bit integer type in Delphi.
  Result := '';

  // Unformatted float coming in? Detect by order of thousand and decimal char
  if ((Pos(',', Str) > 0) and (Pos(',', Str) < Pos('.', Str))) or
    ((Pos('.', Str) > 0) and (Pos('.', ReverseString(Str)) <> 4)) then
  begin
    Str := StringReplace(Str, '.', '*', [rfReplaceAll]);
    Str := StringReplace(Str, ',', FormatSettings.ThousandSeparator, [rfReplaceAll]);
    Str := StringReplace(Str, '*', FormatSettings.DecimalSeparator, [rfReplaceAll]);
  end;

  HasDecimalSep := False;
  for i := 1 to Length(Str) do
  begin
    if CharInSet(Str[i], NumberChars) or ((Str[i] = '-') and (Result = '')) then
    begin
      // Avoid confusion and AV in StrToFloat()
      if (FormatSettings.ThousandSeparator = FormatSettings.DecimalSeparator) and
        (Str[i] = FormatSettings.DecimalSeparator) then
        continue;
      // Ensure only 1 decimalseparator is left
      if (Str[i] = FormatSettings.DecimalSeparator) and HasDecimalSep then
        continue;
      if Str[i] = FormatSettings.DecimalSeparator then
        HasDecimalSep := True;
      if Str[i] = FormatSettings.ThousandSeparator then
        Continue;
      Result := Result + Str[i];
    end
    else
      Break;
  end;
  if (Result = '') or (Result = '-') then
    Result := '0';
end;


{***
  Convert a string-number to an floatingpoint-number

  @param String text representation of a number
  @return Extended
}
function MakeFloat(Str: string): extended;
var
  p_kb, p_mb, p_gb, p_tb, p_pb: integer;
begin
  // Convert result to a floating point value to ensure
  // we don't discard decimal digits for the next step
  Result := StrToFloat(CleanupNumber(Str));

  // Detect if the string was previously formatted by FormatByteNumber
  // and convert it back by multiplying it with its byte unit
  p_kb := Pos(NAME_KB, Str);
  p_mb := Pos(NAME_MB, Str);
  p_gb := Pos(NAME_GB, Str);
  p_tb := Pos(NAME_TB, Str);
  p_pb := Pos(NAME_PB, Str);

  if (p_kb > 0) and (p_kb = Length(Str) - Length(NAME_KB) + 1) then
    Result := Result * SIZE_KB
  else if (p_mb > 0) and (p_mb = Length(Str) - Length(NAME_MB) + 1) then
    Result := Result * SIZE_MB
  else if (p_gb > 0) and (p_gb = Length(Str) - Length(NAME_GB) + 1) then
    Result := Result * SIZE_GB
  else if (p_tb > 0) and (p_tb = Length(Str) - Length(NAME_TB) + 1) then
    Result := Result * SIZE_TB
  else if (p_pb > 0) and (p_pb = Length(Str) - Length(NAME_PB) + 1) then
    Result := Result * SIZE_PB;
end;

{***
  Convert a string-number to an integer-number

  @param string String-number
  @return int64
}
function MakeInt(Str: string): int64;
begin
  // Result has to be of integer type
  try
    Result := Trunc(MakeFloat(Str));
  except
    Result := 0;
  end;
end;

{**
  Unformat a formatted integer or float. Used for CSV export and composing WHERE clauses for grid editing.
}
function UnformatNumber(Val: string): string;
var
  i: integer;
  HasDecim: boolean;
  c: char;
const
  Numbers = ['0'..'9'];
begin
  Result := '';
  HasDecim := False;
  for i := 1 to Length(Val) do
  begin
    c := Val[i];
    if (c = '-') and (i = 1) then
      Result := Result + c
    else if CharInSet(c, Numbers) then
    begin
      if (c = '0') and (Result = '') then
      // remove zeropadding
      else
        Result := Result + c;
    end
    else if (c = FormatSettings.DecimalSeparator) and (not HasDecim) then
    begin
      if Result = '' then
        Result := '0';
      Result := Result + '.';
      HasDecim := True;
    end
    else if c <> FormatSettings.ThousandSeparator then
      break;
  end;
  if Result = '' then
    Result := '0';
end;


{***
  Return a formatted integer or float from a string
  @param string Text containing a number
  @return string
}
function FormatNumber(str: string; Thousands: boolean = True): string; overload;
var
  i, p, Left: integer;
begin
  Result := StringReplace(str, '.', FormatSettings.DecimalSeparator, [rfReplaceAll]);
  if Thousands then
  begin
    // Do not add thousand separators to zerofilled numbers
    if ((Length(Result) >= 1) and (Result[1] = '0')) or
      ((Length(Result) >= 2) and (Result[1] = '-') and (Result[2] = '0')) then
      Exit;
    p := Pos(FormatSettings.DecimalSeparator, Result);
    if p = 0 then p := Length(Result) + 1;
    Left := 2;
    if (Length(Result) >= 1) and (Result[1] = '-') then
      Left := 3;
    if p > 0 then for i := p - 1 downto Left do
      begin
        if (p - i) mod 3 = 0 then
          Insert(FormatSettings.ThousandSeparator, Result, i);
      end;
  end;
end;



{***
  Return a formatted number from an integer

  @param int64 Number to format
  @return string
}
function FormatNumber(int: int64; Thousands: boolean = True): string; overload;
begin
  Result := FormatNumber(IntToStr(int), Thousands);
end;



{***
  Return a formatted number from a float
  This function is called by two overloaded functions

  @param double Number to format
  @param integer Number of decimals
  @return string
}
function FormatNumber(flt: double; decimals: integer = 0;
  Thousands: boolean = True): string; overload;
begin
  Result := Format('%10.' + IntToStr(decimals) + 'f', [flt]);
  Result := Trim(Result);
  Result := FormatNumber(Result, Thousands);
end;

{**
  Format a filesize to automatically use the best fitting expression
  16 100 000 Bytes -> 16,1 MB
  4 500 Bytes -> 4,5 KB
  @param Int64 Number of Bytes
  @param Byte Decimals to display when bytes is bigger than 1M
}
function FormatByteNumber(Bytes: int64; Decimals: byte = 1): string; overload;
begin
  if Bytes >= FSIZE_PB then
    Result := FormatNumber(Bytes / SIZE_PB, Decimals) + NAME_PB
  else if Bytes >= FSIZE_TB then
    Result := FormatNumber(Bytes / SIZE_TB, Decimals) + NAME_TB
  else if Bytes >= FSIZE_GB then
    Result := FormatNumber(Bytes / SIZE_GB, Decimals) + NAME_GB
  else if Bytes >= FSIZE_MB then
    Result := FormatNumber(Bytes / SIZE_MB, Decimals) + NAME_MB
  else if Bytes >= FSIZE_KB then
    Result := FormatNumber(Bytes / SIZE_KB, Decimals) + NAME_KB
  else
    Result := FormatNumber(Bytes) + NAME_BYTES;
end;


{**
  An overloaded function of the previous one which can
  take a string as input
}
function FormatByteNumber(Bytes: string; Decimals: byte = 1): string; overload;
begin
  Result := FormatByteNumber(MakeInt(Bytes), Decimals);
end;


{**
  Format a number of seconds to a human readable time format
  @param Cardinal Number of seconds
  @result String 12:34:56.7
}
function FormatTimeNumber(Seconds: double; DisplaySeconds: boolean;
  MilliSecondsPrecision: integer = 1): string;
var
  d, h, m, s, ms: integer;
  msStr: string;
begin
  s := TruncDef(Seconds, 0);
  ms := TruncDef((Seconds - s) * Power(10, MilliSecondsPrecision), 0);
  // Milliseconds, with variable precision/digits
  msStr := IntToStr(ms).PadLeft(MilliSecondsPrecision, '0');
  d := s div (60 * 60 * 24);
  s := s mod (60 * 60 * 24);
  h := s div (60 * 60);
  s := s mod (60 * 60);
  m := s div 60;
  s := s mod 60;
  if d > 0 then
  begin
    if DisplaySeconds then
    begin
      Result := Format('%d ' + _('days') + ', %.2d:%.2d:%.2d', [d, h, m, s]);
      Result := Result + '.' + msStr; // Append milliseconds
    end
    else
    begin
      Result := Format('%d ' + _('days') + ', %.2d:%.2d h', [d, h, m]);
    end;
  end
  else
  begin
    if DisplaySeconds then
    begin
      Result := Format('%.2d:%.2d:%.2d', [h, m, s]);
      Result := Result + '.' + msStr; // Append milliseconds
    end
    else
    begin
      Result := Format('%.2d:%.2d h', [h, m]);
    end;
  end;
end;

function TruncDef(X: real; Default: int64): int64;
begin
  try
    Result := Trunc(X);
  except
    on EInvalidOp do Result := Default;
  end;
end;


end.
